#include <stdio.h>
#include <stdlib.h> 
#include <sys/time.h>
#include <math.h>
#include <GLUT/glut.h> 
#include <Accelerate/Accelerate.h>

#define kWindowWidth      800 
#define kWindowHeight     800
#define animateSteps      100
// making these power of 2 let
// us perform % with mask instead
#define OUTERLOOP         (0x100000)
#define VBSIZE            (0x20000)

#define PI 3.14159265

const char vs[] =
"void main()                                   \n"
"{                                             \n"
"  gl_Position = gl_Vertex;                    \n"
"}                                             \n";

const char gs[] =
"uniform float a;                              \n"
"uniform float b;                              \n"
"uniform float c;                              \n"
"uniform float d;                              \n"
"uniform vec4 color;                           \n"
"                                              \n"
"void main ()                                  \n"
"{                                             \n"
"  float x = gl_PositionIn[0].x;               \n"
"  float y = gl_PositionIn[0].y;               \n"
"                                              \n"
"  // set color                                \n"
"  gl_FrontColor = color;                      \n"
"                                              \n"
"  for (int j=0; j<8; j++)                     \n"
"  {                                           \n"
"    float xn = sin(a*y) - cos(b*x);           \n"
"    float yn = sin(c*x) - cos(d*y);           \n"
"                                              \n"
"    x = xn;                                   \n"
"    y = yn;                                   \n"
"                                              \n"
"    // set position                           \n"
"    gl_Position = vec4(x*0.5, y*0.5, 0, 1);   \n"
"                                              \n"
"    // done with the vertex                   \n"
"    EmitVertex();                             \n"
"  }                                           \n"
"}                                             \n";

GLuint v, g, p;
	
float *vertexArray = NULL;
GLuint VertexBuffer;

float a,b,c,d;
float a0, b0, c0, d0;
float deltaA, deltaB, deltaC, deltaD;

inline float random(float min, float max)
{
  return (float)rand() / (float)RAND_MAX * (float)(max-min) + (float)min;
}

void setRandom()
{
  a = random(0, PI);
  b = random(0, PI);
  c = random(0, PI);
  d = random(0, PI);
}

void keyboard(unsigned char key, int x, int y)
{
  if (key == 27)
    glutPositionWindow(100, 100);  
}

void display()
{
  static timeval t0 = {0};
  timeval t1;
  gettimeofday(&t1, NULL);
  if (t0.tv_sec != 0)
  {
    printf("%f\n", (double)(t1.tv_sec-t0.tv_sec)+(double)(t1.tv_usec-t0.tv_usec)/1000000);
  }
  t0 = t1;

  static unsigned int times = 0;
  
  if (times == animateSteps)
  {
    times = 0;
  }

  if (times == 0)
  {
    a0 = a;
    b0 = b;
    c0 = c;
    d0 = d;

    setRandom();

    deltaA = (a - a0)/animateSteps;
    deltaB = (b - b0)/animateSteps;
    deltaC = (c - c0)/animateSteps;
    deltaD = (d - d0)/animateSteps;

    a = a0;
    b = b0;
    c = c0;
    d = d0;
  }

  // Set uniforms
  glUniform1f(glGetUniformLocation(p, "a"), a);
  glUniform1f(glGetUniformLocation(p, "b"), b);
  glUniform1f(glGetUniformLocation(p, "c"), c);
  glUniform1f(glGetUniformLocation(p, "d"), d);
  glUniform4f(glGetUniformLocation(p, "color"), 1.f/255, 1.f/255, 1.f/255, 0);

  glClear(GL_COLOR_BUFFER_BIT);

  // Map VBO
  vertexArray = (float *)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);

  for (int i=0; i<OUTERLOOP; i++)
  {
    vertexArray[(((i)&(VBSIZE-1))<<1)+0] = random(-2, 2);
    vertexArray[(((i)&(VBSIZE-1))<<1)+1] = random(-2, 2);

    if ((i)%VBSIZE == VBSIZE-1)
    {
      glUnmapBuffer(GL_ARRAY_BUFFER);
      glDrawArrays(GL_POINTS, 0, VBSIZE);
      glFlush();
      glBufferData(GL_ARRAY_BUFFER, 2*VBSIZE*sizeof(float), NULL, GL_DYNAMIC_DRAW);
      vertexArray = (float *)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
    }
  }

  glUnmapBuffer(GL_ARRAY_BUFFER);
  
  glutSwapBuffers();

  a += deltaA;
  b += deltaB;
  c += deltaC;
  d += deltaD;
  
  times++;
} 

void shaderSetup(void)
{
  v = glCreateShader(GL_VERTEX_SHADER);
  g = glCreateShader(GL_GEOMETRY_SHADER_EXT);

  const char * vv = vs;
  const char * gg = gs;

  glShaderSource(v, 1, &vv, NULL);
  glShaderSource(g, 1, &gg, NULL);

  glCompileShader(v);
  glCompileShader(g);

  p = glCreateProgram();

  glAttachShader(p, v);
  glAttachShader(p, g);

  glProgramParameteriEXT(p, GL_GEOMETRY_INPUT_TYPE_EXT, GL_POINTS);
  glProgramParameteriEXT(p, GL_GEOMETRY_OUTPUT_TYPE_EXT, GL_POINTS);
  glProgramParameteriEXT(p, GL_GEOMETRY_VERTICES_OUT_EXT, 8);

  glLinkProgram(p);
  glUseProgram(p);
}

void graphicsSetup(void)
{
  // clear and set color
  glClearColor(1, 1, 1, 1);
  glClear(GL_COLOR_BUFFER_BIT);

  // setup alpha
  glEnable(GL_BLEND);
  glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
  glBlendFunc(GL_ONE, GL_ONE);

  // setup point render params
  // default point params work well!
  // glPointSize(1);

  // load identity
  glLoadIdentity();

  // create and set VBO
  glGenBuffers(1, &VertexBuffer);
  glBindBuffer(GL_ARRAY_BUFFER, VertexBuffer);
  glBufferData(GL_ARRAY_BUFFER, 2*VBSIZE*sizeof(float), NULL, GL_DYNAMIC_DRAW);
  glEnableClientState(GL_VERTEX_ARRAY);
  glVertexPointer(2, GL_FLOAT, 0, NULL);

  // shader setup
  shaderSetup();
}

int main(int argc, char** argv) 
{
  //setup initial values
  srand( time(NULL) );

  glutInit(&argc, argv); 
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB); 
  glutInitWindowSize(kWindowWidth, kWindowHeight); 
  glutInitWindowPosition(100, 100); 
  glutCreateWindow("Peter De Jong");

  //glutFullScreen();
  
  graphicsSetup();

  setRandom();

  glutKeyboardFunc(keyboard);

  glutDisplayFunc(display);

  glutIdleFunc(display);

  glutMainLoop();

  return 0;
}
